package com.teckteam.ptvsystem.model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.StringTokenizer;

import com.teckteam.ptvsystem.controller.JourneyPlanException;
import com.teckteam.ptvsystem.model.facade.PTVConstants;

/**
 * Timetable system class, used for actually operating the journey planner
 * system
 * 
 * Programmer note: capturedTimetableObject represents the timetable scrape
 * object, organised transportStopList represents James' implementation of
 * adding to system as objects, instead of traversing in one massive
 * LinkedHashMap.
 * 
 * TODO Both are preserved for debugging purposes until we can finalise the
 * logistics of data organisation in full.
 */
public class TimetableSystem
{
   private ArrayList<LinkedHashMap<String, String[]>> capturedTimetableObject;
   private ArrayList<AbstractRoute>                   transportRouteList;
   private ArrayList<String>                          suburbList;
   private ArrayList<AbstractStop>                    transportStopList;
   
   public TimetableSystem()
   {
      capturedTimetableObject = new ArrayList<LinkedHashMap<String, String[]>>();
      transportRouteList = new ArrayList<AbstractRoute>();
      transportStopList = new ArrayList<AbstractStop>();
   }
   
   public ArrayList<String> getAllGroupedRoutes(char flag)
   {
      // Read from one of the above arrays to obtain all train lines.
      // note: all TrainLine instances return getTransportType as "Train"
      
      ArrayList<String> transportRoutes = new ArrayList<String>();
      String setCompare = parseTransportMode(flag);
      
      for (AbstractRoute a : transportRouteList)
      {
         if (a.getTransportType().compareTo(setCompare) == 0)
            transportRoutes.add(a.toString());
      }
      
      return transportRoutes;
   }
   
   public ArrayList<AbstractStop> getAllGroupedStops(char flag)
   {
      // Read from specified and collect specified stop objects
      ArrayList<AbstractStop> stopList = new ArrayList<AbstractStop>();
      String setCompare = parseTransportMode(flag);
      
      for (AbstractStop s:transportStopList)
      {
         switch(flag)
         {
            case PTVConstants.TRAIN_FLAG:
               if(s instanceof TrainStation)
                  stopList.add(s);
               break;
            case PTVConstants.TRAM_FLAG:
               if(s instanceof TramStop)
                  stopList.add(s);
               break;
            case PTVConstants.BUS_FLAG:
               if(s instanceof BusStop)
                  stopList.add(s);
               break;
         }
      }
      
      //Collections.sort(stopList);
      return stopList;
   }
   
   public ArrayList<AbstractStop> getAllStops()
   {
      return transportStopList;
   }
   
   /**
    * Returns list of relevant suburbs present in the system based on stop information.
    * Will collect from the system only once.
    */
   public ArrayList<String> getAllSuburbs()
   {
      //Return what's already been collected
      if(suburbList!=null)
         return suburbList;
      
      ArrayList<String> suburbList = new ArrayList<String>();
      Set filterSet = new HashSet();
      
      for (AbstractRoute r : transportRouteList)
      {
         ArrayList<String> subList = r.getSuburbList();
         for (String sub : subList)
         {
            if (filterSet.add(sub))
               suburbList.add(sub);
         }
      }
      
      //Sort and save what's already been collected to memory
      Collections.sort(suburbList);
      this.suburbList = suburbList;
      return suburbList;
   }
   
   /**
    * Returns the arrival time with respect to departure, and the stops. 
    * 
    * @param departureTimeObj
    * @param stopFrom
    * @param route
    * @param stopTo
    */
   public DepartureTime getArrivalTime(DepartureTime departureTimeObj, AbstractStop stopFrom,
         AbstractRoute route, AbstractStop stopTo) throws JourneyPlanException
   {
      ArrayList<DepartureTime> arrivalTimes =
            route.getVehicleTripFromTime(departureTimeObj, stopFrom);
      
      int destIndex = route.getStopIndex(stopTo);
      if (destIndex==PTVConstants.FAILED_COMPARE) throw new JourneyPlanException();
      
      DepartureTime d = arrivalTimes.get(destIndex);
      
      return d;
   }
   
   /**
    * Returns a list of interchanging stops 
    * @param first
    * @param second
    * @return
    */
   public ArrayList<AbstractStop> getInterchangeList(AbstractRoute first,
         AbstractRoute second)
   {
      // TODO - get list of interchange stations based on line
      ArrayList<AbstractStop> interchangeList = new ArrayList<AbstractStop>();
      
      for (AbstractStop s : first.getStopList())
      {
         for (AbstractStop t : second.getStopList())
         {
            System.out.println(String.format("%s vs %s", s.toString(),
                  t.toString()));
            if (s.toString().compareTo(t.toString()) == 0)
            {
               interchangeList.add(t);
            }
               
         }
      }
      
      return interchangeList;
   }
   
   /**
    * Obtains the nearest stop based on locality data.  (Brute-force)
    * 
    * @param startStop
    * @return
    */
   public AbstractStop getNearestStop(AbstractStop stop)
   {
      double distance = 0;
      AbstractStop closestStop = null;
      
      for (AbstractStop s : transportStopList)
      {
         double distResult = s.getLocality().compareDistanceTo(stop.getLocality());
         if(distResult!=0 && (distance==0 || distResult < distance))
         {
            distance = distResult;
            closestStop = s;
         }
      }
      
      System.out.printf("Closest stop to %s is %s (%d km)\n", stop.toString(), closestStop.toString(), distance);
      return closestStop;
   }
   
   
   /**
    * Collects all routes containing the same stop name.
    * 
    * @param stopList
    * @return routeList
    */
   public ArrayList<AbstractRoute> getPossibleRoutes(
         ArrayList<AbstractStop> stopList)
   {
      ArrayList<AbstractRoute> routeList = new ArrayList<AbstractRoute>();
      
      for(AbstractStop s : stopList)
         routeList.add(getRouteFromStop(s));
      
      return routeList;
   }
   
   public AbstractRoute getRoute(String name/*, char dirFlag*/)
   {
      /*String s = parseDirectionMode(dirFlag); 
      
      name = String.format("%s(%s)", name, s);*/
      
      for (AbstractRoute r : transportRouteList)
      {
         if (name.compareTo(r.toString()) == 0)
         {
            return r;
         }
      }     
      
      return null;
   }
   
   /**
    * DEBUG: Hacky implementation of obtaining routes, currently used for testing
    * Uses the scraed objects as a source.
    */
   public LinkedHashMap<String, String[]> getRouteDebug(String name)
   {
      LinkedHashMap<String, String[]> foundRoute;
      
      for (LinkedHashMap<String, String[]> timeList : capturedTimetableObject)
      {
         // TODO DIFFERENTIATE different transport types
         
         StringTokenizer st = new StringTokenizer(name, ":(");
         st.nextToken();
         String s = String.format("%s", st.nextToken()), t = timeList
               .get("routeHeader")[0];
         if (t.compareTo(s.substring(1, s.length() - 1)) == 0)
         {
            s = st.nextToken("()");
            if (s.compareTo("Inbound") == 0
                  && timeList.get("routeHeader")[2].charAt(0) == PTVConstants.TIMETABLE_INBOUND
                  || s.compareTo("Outbound") == 0
                  && timeList.get("routeHeader")[2].charAt(0) == PTVConstants.TIMETABLE_OUTBOUND)
            {
               s = st.nextToken();
               if (s.substring(3).compareTo("Mon - Fri") == 0
                     && timeList.get("routeHeader")[1]
                           .compareTo(PTVConstants.TIMETABLE_MONFRI) == 0
                     || s.substring(3).compareTo("Saturday") == 0
                     && timeList.get("routeHeader")[1]
                           .compareTo(PTVConstants.TIMETABLE_SAT) == 0
                     || s.substring(3).compareTo("Sunday") == 0
                     && timeList.get("routeHeader")[1]
                           .compareTo(PTVConstants.TIMETABLE_SUN) == 0
                     || s.substring(3).compareTo("Friday") == 0
                     && timeList.get("routeHeader")[1]
                           .compareTo(PTVConstants.TIMETABLE_FRI) == 0
                     || s.substring(3).compareTo("Mon - Thur") == 0
                     && timeList.get("routeHeader")[1]
                           .compareTo(PTVConstants.TIMETABLE_MONTHUR) == 0)
                  return timeList;
            }
         }
         
      }
      return null;
   }
   
   /**
    * Gets a stop's corresponding route, based on where the stop object exists.
    * @param stopObject
    * @param directionFlag
    * @return r
    */
   public AbstractRoute getRouteFromStop(AbstractStop stopObject)
   {  
      for (AbstractRoute r : transportRouteList)
      {
         ArrayList<AbstractStop> stopList = r.getStopList();
         for(AbstractStop s:stopList)
         {
            if (s == stopObject)
            {
               //if(r.getDirectionFlag()==directionFlag)
                  return r;
            }
         }
      }
      
      // If not found (not supposed to reach here)
      return null;
   }
   
   /**
    * Gets the first stop on the list that matches the parameters.
    * 
    * @param name
    * @param directionFlag
    * @return
    */
   public AbstractStop getStop(String name, char directionFlag)
   {
      for (AbstractStop s : transportStopList)
      {
         if (s.toString().compareTo(name) == 0)
         {
            //TODO check direction match
            return s;
         }
      }
      
      return null;
   }

   /**
    * Debug: return all stop objects with the same stop name.
    * DEVELOPER NOTE: TO BE REMOVED AFTER OBJECT PARSER REWRITE
    * @param source
    * @return
    */
   public ArrayList<AbstractStop> getStopInstances(String name)
   {
      ArrayList<AbstractStop> instanceList = new ArrayList<AbstractStop>();
      for (AbstractStop s : transportStopList)
      {
         if (s.toString().compareTo(name) == 0)
            instanceList.add(s);
      }
      
      if(instanceList.size()==0) 
         return null;
      
      return instanceList;
   }
   
   /**
    * Returns ArrayList of AbstractStop objects with respect to their
    * suburb.
    */
   public ArrayList<AbstractStop> getSuburbStops(String suburb)
   {
      ArrayList<AbstractStop> foundStops = new ArrayList<AbstractStop>();
      Set filterList = new HashSet();
      
      // TODO - overloaded method to search for stops in a suburb directly
      for (AbstractRoute r : transportRouteList)
      {
         for (AbstractStop s : r.getStopList())
         {
            if (s.getSuburb().toString().compareTo(suburb) == 0)
               if (filterList.add(s.toString()))
                  foundStops.add(s);
         }
      }
      
      return foundStops;
   }
   
   /**
    * Populate system with scraped route timetable to usable information
    * This serves as an object parser for facilitating the content needed
    * for central functionality.
    * 
    * @param timeTable
    * @throws Exception
    */
   public void importRouteInfo(LinkedHashMap<String, String[]> timeTable)
         throws Exception
   {
      AbstractRoute r = getRoute(timeTable.get("routeHeader")[0]);
      
      // TODO Check if Route with same name already exists
      // if(r!=null) {
      // //if yes, check routeHeader if different flags
      // return;
      // }
      
      // differentiate modes of transport on adding routes
      switch (timeTable.get("routeHeader")[4].charAt(0)) {
         case PTVConstants.TRAIN_FLAG:
            r = new TrainLine(timeTable);
            break;
         case PTVConstants.BUS_FLAG:
            r = new BusRoute(timeTable);
            break;
         case PTVConstants.TRAM_FLAG:
            r = new TramRoute(timeTable);
            break;
         default:
            throw new Exception(String.format(
                  "Failed to load %s due to invalid data.",
                  timeTable.get("routeHeader")[0]));
      }
      
      // Initialize stops
      r.createStops(timeTable);
      
      capturedTimetableObject.add(timeTable);
      transportRouteList.add(r);
      for(AbstractStop s:r.getStopList())
         transportStopList.add(s);
   }
   
   /**
    * Returns if the Timetable system is currently empty.
    */
   public boolean isEmpty()
   {
      if (capturedTimetableObject.size() < 1 || transportRouteList.size() < 1)
         return true;
      
      return false;
   }
   
   private String parseDirectionMode(char flag)
   {
      switch (flag) 
      {
         case PTVConstants.TIMETABLE_INBOUND:
            return "Inbound";
         case PTVConstants.TIMETABLE_OUTBOUND:
            return "Outbound";
      }
      
      // This is never reached
      return null;
   }

   private String parseTransportMode(char flag)
   {
      switch (flag) 
      {
         case PTVConstants.TRAIN_FLAG:
            return "Train";
         case PTVConstants.TRAM_FLAG:
            return "Tram";
         case PTVConstants.BUS_FLAG:
            return "Bus";
      }
      
      // This is never reached
      return null;
   }
}
